import java.math.BigDecimal;
import java.util.Calendar;
import java.util.Collections;
import java.util.Comparator;
import java.util.LinkedList;

public class Server implements IServer 
{
	private int server_id;
	private long total_server_memory;
	private int processing_units;
	private String serverDescription = "";
	
	private int jobsDueProcessingCount = 0;
	private int runningJobsCount = 0;
	
	private double power_factor;
	private double powerConsumption;
	
	private LinkedList<IJob> unserviced_jobs = null;
	private LinkedList<IProcessingUnit> processing_units_list = null;
	
	protected volatile Boolean running = null;
	
	private Comparator<IJob> compareJobs = new JobComparator();
	private Comparator<IProcessingUnit> compareProcessingUnit = new ProcessingUnitComparator();
	
	public Server(int sID, long serverMemory, double pF, Boolean running, int npu, int pCores, String serverDesc)
	{
		this.server_id = sID;
		this.total_server_memory = serverMemory;
		this.power_factor = calculateAccurateDouble(pF);
		this.processing_units = npu;
		this.serverDescription = serverDesc;
		this.running = running;
		
		processing_units_list = new LinkedList<IProcessingUnit>();
		powerConsumption = 0.0;
		
		unserviced_jobs = new LinkedList<IJob>();
		
		createProcessingUnits(pCores);
		
		startServerThread();
	}
	
	public void assignJob(IJob new_job) 
	{
		unserviced_jobs.add(new_job);
		
		Collections.sort(unserviced_jobs, this.compareJobs);	// Sorts the job in ascending order of priority.
		
		jobsDueProcessingCount++;
	}

	public double getPowerConsumption() 
	{
		return calculateAccurateDouble(powerConsumption);
	}

	public int getServerID() 
	{
		return server_id;
	}
	
	public int getTotalNPEValue()
	{
		int count = 0;
		
		for(int i=0;i<this.processing_units;i++)
			count = count + processing_units_list.get(i).getNumberOfProcessingElements();
		
		return count;
	}
	
	public int getNPECount()
	{
		return processing_units_list.getFirst().getNumberOfProcessingElements();
	}

	public double getMaxPower() 
	{
		return (memorytoMegaBytes(total_server_memory) * power_factor * 2);	// Theoretically Maximum (Worst case scenario) in Watt Hours
	}
	
	public double getPowerFactor()
	{
		return power_factor;
	}
	
	public String getServerDesc()
	{
		return this.serverDescription;
	}
	
	public String getSystemStatus()
	{
		String systemStatus = "";
		
		for(int i=0;i<this.processing_units;i++)
			systemStatus = systemStatus + processing_units_list.get(i).getSystemState() + ", ";
		
		return systemStatus;
	}
	
	private double calculateAccurateDouble(double value)
	{
		BigDecimal decimal = new BigDecimal(value);
		decimal = decimal.setScale(2, BigDecimal.ROUND_HALF_UP);
		
		return decimal.doubleValue();
	}
	
	public int getRunningJobCount()
	{
		int count = 0;
		
		for(int i=0;i<this.processing_units;i++)
			count = count + processing_units_list.get(i).getRunningJobCount();
		
		return count;
		
		//return runningJobsCount;
	}
	
	public int getDueJobCount()
	{
		return unserviced_jobs.size();
		
		//return jobsDueProcessingCount;
	}
	
	public int getNPUCount()
	{
		return this.processing_units;
	}
	
	public IProcessingUnit getProcessingUnit(int index)
	{
		return processing_units_list.get(index);
	}
	
	public long getServerMemory()
	{
		return this.total_server_memory;
	}
	
	public long getServerFreeMemory()
	{
		long freeMemory = 0L;
		
		for(int i=0;i<processing_units;i++)
			freeMemory = freeMemory + processing_units_list.get(i).getFreeMemory();
		
		return freeMemory;
	}
	
	public IJob getUnservicedJob(int index) throws Exception
	{
		return unserviced_jobs.get(index);
	}
	
	private void startServerThread()
	{
		Runnable serverThread = new Runnable()
		{
			public void run()
			{
				// Thread responsible for distributing jobs onto various processing units
				// Also responsible for updating the state of the server by polling various processors(NPUs)
				
				while(running)
				{
					if(!unserviced_jobs.isEmpty())
					{
						// Distribute Jobs
						
						IJob currentJob = unserviced_jobs.getLast();
						
						if(isStartTime(currentJob))
						{
							// Distribute the job into NPU(s)
							
							Collections.sort(processing_units_list, compareProcessingUnit);
							
							int job_NPE = currentJob.getNPE();
							int server_NPE = processing_units_list.getFirst().getNumberOfProcessingElements();
							
							if(job_NPE == 1)
							{
								// Assign job to a SINGLE NPU
								
								for(int i=0;i<processing_units;i++)
								{
									if(processing_units_list.get(i).getFreeMemory() >= currentJob.getMemoryRequirement())
									{
										processing_units_list.get(i).addJob(currentJob);
										unserviced_jobs.removeLast();
										
										jobsDueProcessingCount--;
										runningJobsCount++;
										
										break;
									}
								}
							}
							
							else if(job_NPE <= server_NPE)
							{
								// Decide on how to distribute the job
								// SINGLE NPU or multiple (possibly 2) NPUs
								
								if(job_NPE <= server_NPE/2)
								{
									// Assign to SINGLE NPU
									
									for(int i=0;i<processing_units;i++)
									{
										if(processing_units_list.get(i).getFreeMemory() >= currentJob.getMemoryRequirement())
										{
											processing_units_list.get(i).addJob(currentJob);
											unserviced_jobs.removeLast();
											
											jobsDueProcessingCount--;
											runningJobsCount++;
											
											break;
										}
									}
								}
								
								else
								{
									// Assign to 2 NPUs
									
									int job1_NPE = job_NPE/2;
									int job2_NPE = job_NPE - job1_NPE;
									
									IJob tempJob1 = createNewJob(currentJob,job1_NPE);
									IJob tempJob2 = createNewJob(currentJob,job2_NPE);
									
									for(int i=0;i<processing_units;i++)
									{
										if(processing_units_list.get(i).getFreeMemory() >= tempJob1.getMemoryRequirement())
										{
											if(processing_units > i+1)	// If there is another NPU after this ith NPU
											{
												processing_units_list.get(i).addJob(tempJob1);
												processing_units_list.get(i+1).addJob(tempJob2);
												
												unserviced_jobs.removeLast();
												
												jobsDueProcessingCount--;
												runningJobsCount++;
												
												break;
											}
											
											else
											{
												// If this is the last NPU, check if it can handle both the jobs
												
												if(processing_units_list.get(i).getFreeMemory()*2 >= tempJob1.getMemoryRequirement())
												{
													processing_units_list.get(i).addJob(tempJob1);
													processing_units_list.get(i).addJob(tempJob2);
													
													unserviced_jobs.removeLast();
													
													jobsDueProcessingCount--;
													runningJobsCount++;
													
													break;
												}
											}
											
											/*processing_units_list.get(i).addJob(tempJob1);
											
											if(processing_units > i+1)
												processing_units_list.get(i+1).addJob(tempJob2);
											else
												processing_units_list.get(i).addJob(tempJob1);
											
											unserviced_jobs.removeLast();
											
											jobsDueProcessingCount--;
											runningJobsCount++;
											
											break;*/
										}
									}
								}
							}
							
							else
							{								
								// Divide the job into multiple NPUs
								
								double npe_ratio = calculateAccurateDouble((double)(job_NPE)/ (double)(server_NPE));
								int count = (int) npe_ratio;
								int index = 0;
								
								if(String.valueOf(npe_ratio).contains("."))
									count++;
								
								int[] processing_unit_indeces = new int[count];
								
								for(int i = 0;i<processing_units;i++)
								{
									if(processing_units_list.get(i).getFreeMemory() >= currentJob.getMemoryRequirement())
										processing_unit_indeces[index++] = i;
									
									if(index >= processing_unit_indeces.length)
										break;
								}
								
								if(index == processing_unit_indeces.length)
								{
									// Required number of NPUs found
									
									for(int i=0;i<(int) (npe_ratio);i++)
									{
										IJob tempJob = createNewJob(currentJob,server_NPE);
										 processing_units_list.get(processing_unit_indeces[i]).addJob(tempJob);
									}
									
									// Create a new job with single NPE and submit only if the ratio is not zero
									
									if((double) (count) > npe_ratio)
									{
										IJob tempJob = createNewJob(currentJob,1);
										processing_units_list.get(processing_unit_indeces[count-1]).addJob(tempJob);
									}
									
									unserviced_jobs.removeLast();
									
									jobsDueProcessingCount--;
									runningJobsCount++;
								}
							}
						}
					}
					
					// Update the server state
					
					double newPowerConsumption = 0.0;
					
					for(int i=0;i<processing_units;i++)
						newPowerConsumption = newPowerConsumption + processing_units_list.get(i).getPowerConsumption();
					
					powerConsumption = newPowerConsumption;
					
					try {
						Thread.sleep(200);
					} catch (InterruptedException e) {
						e.printStackTrace();
					}
				}
			}
		};
		
		Thread serverThreadObject = new Thread(serverThread);
		serverThreadObject.start();
	}
	
	private boolean isStartTime(IJob job)
	{
		int currentHour = Calendar.getInstance().get(Calendar.HOUR_OF_DAY);
		int currentMinute = Calendar.getInstance().get(Calendar.MINUTE);
		int currentSecond = Calendar.getInstance().get(Calendar.SECOND);
		
		if((job.getStartHour() >= 0)
				&& (job.getStartMinute() >= 0)
				&& (job.getStartSecond() >= 0))
		{
			if((job.getStartHour() >= currentHour)
					&& (job.getStartMinute() >= currentMinute)
					&& (job.getStartSecond() >= currentSecond))
				return true;
			
			return false;
		}
		
		return true;
	}
	
	private Job createNewJob(IJob oldJob, int newNPEValue)
	{
		if(newNPEValue > 0)
		{
			Job tempJob =  new Job(oldJob.getJobName(),oldJob.getJobDescription(),oldJob.getMemoryRequirement(),oldJob.getDuration(),oldJob.getJobPriority(),newNPEValue);
			
			tempJob.setStartHour(oldJob.getStartHour());
			tempJob.setStartMinute(oldJob.getStartMinute());
			tempJob.setStartSecond(oldJob.getStartSecond());
			
			return tempJob;
		}
		
		return null;
	}
	
	private void createProcessingUnits(int pCores)
	{
		if((total_server_memory % processing_units) == 0)
		{
			for(int i=0;i<this.processing_units;i++)
				processing_units_list.add(new ProcessingUnit(i,(total_server_memory/processing_units),pCores,power_factor,running));
		}
		
		else
		{
			long extraMemory = total_server_memory % processing_units;
			long proportionateMemory = total_server_memory - extraMemory;
			
			for(int i=0;i<(processing_units - 1);i++)
				processing_units_list.add(new ProcessingUnit(i,(proportionateMemory/processing_units),pCores,power_factor,running));
			
			// Add in the last processor with greater memory
			
			processing_units_list.add(new ProcessingUnit(processing_units -1,((proportionateMemory/processing_units) + extraMemory),pCores,power_factor,running));
		}
	}
	
	private double memorytoMegaBytes(long givenMemory)
	{
		double convertedMemory = 0.0;
		
		convertedMemory = ((double)givenMemory/1024);
		
		return calculateAccurateDouble(convertedMemory);
	}
}
